package com.jo.register;

import com.google.common.collect.Lists;
import com.jo.advisor.*;
import com.jo.annotation.After;
import com.jo.annotation.Aspect;
import com.jo.annotation.Before;
import com.jo.bean.AopBeanDefinition;
import com.jo.bean.BeanDefinition;
import com.jo.bean.Property;
import com.jo.factory.ClassPathXmlApplicationContext;
import com.jo.util.IocUtil;
import com.jo.util.ReflectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

/**
 * aop的ioc容器，集成了之前的ioc容器，扩展了方法
 * 增加了对aop中bean解析
 *
 * @author Jo
 * @date 2018/1/25
 */
public class AopApplicationContext extends ClassPathXmlApplicationContext {

    protected List<Advisor> advisorCache;

    public AopApplicationContext(String configLocation) throws RuntimeException {
        super(configLocation);
    }


    /**
     * 覆盖父类注册bean定义方法，会进行aop注解的解析
     * @param clazz
     */
    @Override
    protected void registerBeanDefinition(Class clazz) {
        //是否是切面类
        if (isAspect(clazz)) {
            ReflectionUtils.doWithMethod(clazz, (method -> {
                registerAdviceMethod(clazz, method);
            }));
            registerAspectBeanDefinition(clazz);
        }
        else {
            //普通bean
            super.registerBeanDefinition(clazz);
        }
    }

    /**
     * 覆盖父类获取bean方法，在获取时，如果需要进行代理，则生成代理类
     * @param name
     * @return
     */
    @Override
    public Object getBean(String name) {

        //如果已经生成则直接返回
        Object bean = this.singletonMap.get(name);
        if (bean != null){
            return bean;
        }

        //如果未生成过，则查看是否需要生成代理对象
        bean = super.getBean(name);
        BeanDefinition beanDefinition = super.getBeanDefinition(name);
        if (beanDefinition != null) {
            //生成代理类
            if (notAopComponent(beanDefinition)) {
                Object proxy = createProxyIfMatch(bean, name);
                if (proxy != null) {
                    replaceBeanByProxy(name, proxy);
                    bean = proxy;
                }
            }
        }
        return bean;
    }

    private boolean notAopComponent(BeanDefinition beanDefinition) {
        return !(beanDefinition instanceof AopBeanDefinition);
    }

    private void replaceBeanByProxy(String name, Object proxy) {
        this.singletonMap.put(name, proxy);
    }

    private void registerAdviceMethod(Class clazz, Method method) {
        if (isAdvice(method)) {
            if (isBefore(method)) {
                registerBeforeAdvice(clazz, method);
            }

            if (isAfter(method)) {
                registerAfterAdvice(clazz, method);
            }
        }
    }

    private void registerAfterAdvice(Class clazz, Method method) {
        AopBeanDefinition aopBeanDefinition = createGenericAopBeanDefinition(clazz, method, false);
        After afterAnnotation = method.getAnnotation(After.class);
        aopBeanDefinition.setIsAdvise(Boolean.TRUE);
        aopBeanDefinition.setIsAfter(Boolean.TRUE);
        aopBeanDefinition.setTargets(afterAnnotation.targets());
        aopBeanDefinition.setClazz(AspectAfterAdvice.class);
        super.registerBeanDefinition(aopBeanDefinition);
    }

    private void registerBeforeAdvice(Class clazz, Method method) {
        AopBeanDefinition aopBeanDefinition = createGenericAopBeanDefinition(clazz, method, true);
        Before beforeAnnotation = method.getAnnotation(Before.class);
        aopBeanDefinition.setIsAdvise(Boolean.TRUE);
        aopBeanDefinition.setIsBefore(Boolean.TRUE);
        aopBeanDefinition.setTargets(beforeAnnotation.targets());
        aopBeanDefinition.setClazz(AspectBeforeAdvice.class);
        super.registerBeanDefinition(aopBeanDefinition);
    }

    private void registerAspectBeanDefinition(Class clazz) {
        BeanDefinition beanDefinition = super.createBeanDefinition(clazz);
        AopBeanDefinition aopBeanDefinition = new AopBeanDefinition();
        BeanUtils.copyProperties(beanDefinition, aopBeanDefinition);
        aopBeanDefinition.setIsAspect(Boolean.TRUE);
        super.registerBeanDefinition(aopBeanDefinition);
    }

    private AopBeanDefinition createGenericAopBeanDefinition(Class clazz, Method method, boolean isBefore) {
        AopBeanDefinition aopBeanDefinition = new AopBeanDefinition();
        String aspectName = IocUtil.getMyComponentName(clazz);
        String methodName = method.getName();
        String adviseName = getAdviceName(aspectName, methodName, isBefore);

        aopBeanDefinition.setBeanId(adviseName);
        aopBeanDefinition.setBeanName(adviseName);


        aopBeanDefinition.setAdviceMethod(method);
        aopBeanDefinition.setAspectName(aspectName);

        //到时候会通过构造函数实例化AspectAfterAdvice或AspectBeforeAdvice对应的构造函数
        List<Property> constructorArgs = Lists.newLinkedList();
        constructorArgs.add(new Property(methodName, method, null, Method.class));
        constructorArgs.add(new Property(aspectName, null, aspectName, Object.class));

        aopBeanDefinition.setConstructorArgs(constructorArgs);
        return aopBeanDefinition;
    }

    private Object createProxyIfMatch(Object target, String name) {
        Object proxy = null;

        List<Advisor> candidateAdvisor = findAllCandidateAdvisor();
        List<Advisor> eligibleAdvisor = new LinkedList<>();
        for (Advisor advisor : candidateAdvisor) {
            if (canApply(advisor, name)) {
                eligibleAdvisor.add(advisor);
            }
        }

        if (!CollectionUtils.isEmpty(eligibleAdvisor)) {
            AdvisedSupport adviseSupport = getAdviseSupport(eligibleAdvisor, target);
            proxy = ProxyFactory.createProxy(adviseSupport);
        }

        return proxy;
    }

    private boolean canApply(Advisor advisor, String targetBeanName) {
        Pointcut pointcut = advisor.getPointcut();

        return pointcut.match(targetBeanName);
    }

    /**
     * 查找所有的容器中的advisor
     *
     * @return
     */
    private List<Advisor> findAllCandidateAdvisor() {
        if (advisorCache != null) {
            return advisorCache;
        }

        List<Advisor> candidateAdvisor = new LinkedList<>();

        // TODO: 2018/1/26 可以做缓存
        beanDefinitionMap.forEach((beanName, beanDefinition) -> {
            if (beanDefinition instanceof AopBeanDefinition) {
                AopBeanDefinition aopBeanDefinition = (AopBeanDefinition) beanDefinition;

                if (aopBeanDefinition.getIsAdvise()) {
                    Advisor advisor = createAdvisor(aopBeanDefinition);

                    candidateAdvisor.add(advisor);
                }
            }
        });

        return advisorCache = candidateAdvisor;
    }

    private Advisor createAdvisor(AopBeanDefinition aopBeanDefinition) {
        Advisor advisor = new Advisor();
        advisor.setAdvice(getAdvise(aopBeanDefinition));
        advisor.setPointcut(new DefaultPointcut(Lists.newArrayList(aopBeanDefinition.getTargets())));

        return advisor;
    }

    private Advice getAdvise(AopBeanDefinition aopBeanDefinition) {
        Method adviseMethod = aopBeanDefinition.getAdviceMethod();
        String adviseName = getAdviceName(aopBeanDefinition.getAspectName(), adviseMethod.getName(), aopBeanDefinition.getIsBefore());
        return (Advice) super.getBean(adviseName);
    }

    private String getAdviceName(String aspectName, String methodName, boolean isBefore) {

        return aspectName +
                "." +
                methodName +
                "." +
                (isBefore ? "before" : "after");
    }

    private AdvisedSupport getAdviseSupport(List<Advisor> eligibleAdvisor, Object target) {
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTargetSource(new TargetSource(target));
        advisedSupport.setAdvisors(eligibleAdvisor);

        return advisedSupport;
    }

    private boolean isAspect(Class clazz) {
        Annotation annotation = clazz.getAnnotation(Aspect.class);
        return annotation != null;
    }

    private boolean isBefore(Method method) {
        Annotation annotation = method.getAnnotation(Before.class);
        return annotation != null;
    }

    private boolean isAfter(Method method) {
        Annotation annotation = method.getAnnotation(After.class);
        return annotation != null;
    }

    private boolean isAdvice(Method method) {
        return isBefore(method) || isAfter(method);
    }
}
